package com.google.code.mmry;

import java.lang.reflect.Proxy;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.InvalidMarkException;

public class XLByteBuffer {

  private final ByteBuffer[] buffers;
  private final long capacity;
  private final boolean direct;
  private long position;
  private long limit;
  private long mark = -1;

  /**
   * Create a new <tt>XLByteBuffer</tt> with the given capacity, either direct
   * or heap allocated.
   * 
   * @param capacity
   *          The size of the {@link XLByteBuffer} to allocate, in bytes.
   * @param direct
   *          Weather or not to allocate the underlying buffers using
   *          {@link ByteBuffer#allocateDirect(int)}.
   */
  public XLByteBuffer(long capacity, boolean direct) {
    int numBigBuffers = (int) (capacity / (Integer.MAX_VALUE));
    int lastBufferSize = (int) (capacity % (Integer.MAX_VALUE));
    int numBuffersTotal = (int) numBigBuffers + (lastBufferSize != 0 ? 1 : 0);
    buffers = new ByteBuffer[numBuffersTotal];
    for (int i = 0; i < numBigBuffers; i++) {
      buffers[i] = direct ? ByteBuffer.allocateDirect(Integer.MAX_VALUE)
          : ByteBuffer.allocate(Integer.MAX_VALUE);
    }
    if (lastBufferSize != 0) {
      buffers[numBigBuffers] = direct ? ByteBuffer
          .allocateDirect(lastBufferSize) : ByteBuffer.allocate(lastBufferSize);
    }
    this.direct = direct;
    this.capacity = capacity;
  }

  private XLByteBuffer(ByteBuffer[] buffers, long capacity, boolean direct) {
    this.buffers = buffers;
    this.capacity = capacity;
    this.direct = direct;
  }

  private static int bufferA(long originalAddress) {
    return (int) (originalAddress / (Integer.MAX_VALUE));
  }

  private static int byteA(long originalAddress) {
    return (int) (originalAddress % (Integer.MAX_VALUE));
  }

  /**
   * Return the capacity of this buffer in bytes.
   */
  public final long capacity() {
    return capacity;
  }

  /**
   * Return the current position of this XLByteBuffer
   * 
   * @return
   */
  public final long position() {
    return position;
  }

  /**
   * Set this position of this buffer's cursor. If the current mark exceeds the
   * new position, it is reset.
   * 
   * @param newPosition
   *          The new position of the cursor. If a position greater than the
   *          capacity of the buffer or the limit is passed, an
   *          {@link IllegalArgumentException} is thrown.
   */
  public final XLByteBuffer position(long newPosition) {
    if (newPosition > limit || newPosition < 0) {
      throw new IllegalArgumentException();
    }
    if (mark > position) {
      mark = -1;
    }
    position = newPosition;
    return this;
  }

  /**
   * Get the limit of this <tt>XLByteBuffer</tt>.
   */
  public final long limit() {
    return limit;
  }

  /**
   * Set the limit of this <tt>XLByteBuffer</tt>. Throw an
   * {@link IllegalArgumentException} if the limit exceeds the capacity of the
   * buffer.
   */
  public final XLByteBuffer limit(long newLimit) {
    if (limit > capacity || limit < 0) {
      throw new IllegalArgumentException();
    }
    return this;
  }

  /**
   * Get the current to the current position.
   */
  public final XLByteBuffer mark() {
    this.mark = position;
    return this;
  }

  /**
   * Reset the current position to the last mark. If no mark has been set, a
   * {@link InvalidMarkException} is thrown.
   */
  public final XLByteBuffer reset() {
    if (this.mark < 0) {
      throw new InvalidMarkException();
    }
    this.position = this.mark;
    return this;
  }

  /**
   * Reset the state of this buffer so that the mark is unset, the position is
   * 0, and the limit is the capacity.
   */
  public final XLByteBuffer clear() {
    this.mark = -1;
    this.position = 0;
    this.limit = capacity;
    return this;
  }

  /**
   * Set the position to 0 and the limit to the current position. Used to switch
   * to "read mode" after writing data into the buffer via a sequence of puts.
   */
  public final XLByteBuffer flip() {
    this.mark = -1;
    this.limit = position;
    this.position = 0;
    return this;
  }

  /**
   * Set this buffer's position to 0 and discard it's mark.
   */
  public final XLByteBuffer rewind() {
    this.position = 0;
    this.mark = -1;
    return this;
  }

  /**
   * Get the difference of the limit and the position.
   */
  public final long remaining() {
    return limit - position;
  }

  /**
   * Return true if this buffer has any data left between the position and the
   * limit
   */
  public final boolean hasRemaining() {
    return limit - position != 0;
  }

  public boolean isReadOnly() {
    return false;
  }

  public XLByteBuffer duplicate() {
    // TODO implement
    return this;
  }

  public XLByteBuffer asReadOnlyBuffer() {
    return new ReadOnlyXLByteBuffer(buffers, capacity, direct);
  }

  public byte get() {
    // TODO implement
    return 0;
  }

  public XLByteBuffer put(byte b) {
    // TODO implement
    return this;
  }

  public byte get(long index) {
    // TODO implement
    return 0;
  }

  public XLByteBuffer put(long index, byte b) {
    // TODO implement
    return this;
  }

  public XLByteBuffer get(byte[] dst, long offset, int length) {
    // TODO implement
    return this;
  }

  public XLByteBuffer get(byte[] dst) {
    // TODO implement
    return this;
  }

  public XLByteBuffer put(ByteBuffer src) {
    // TODO implement
    return this;
  }

  public XLByteBuffer put(XLByteBuffer src) {
    // TODO implement
    return this;
  }

  public XLByteBuffer put(byte[] src, long offset, int length) {
    // TODO implement
    return this;
  }

  public final XLByteBuffer put(byte[] src) {
    // TODO implement
    return this;
  }

  public boolean isDirect() {
    return direct;
  }

  public ByteBuffer subBuffer(long offset, int length) {
    // TODO implement
    /*
     * supposed to be sort of like "String.subSequence" but for bytes. To do
     * this right, we need a new bytebuffer that can basically delegate to this
     * class as though it is the underlying byte array to "wrap". One way to do
     * this might be to create a new clas in java.nio -- I think its probably
     * better to use cglib to generate the "mock" bytebuffer though.
     */
    return null;
  }

  /**
   * An almost-decorator class that wraps the underlying bytebuffers from it's
   * parent, but provides its own position, mark, and limit.
   */
  private static class ReadOnlyXLByteBuffer extends XLByteBuffer {

    public ReadOnlyXLByteBuffer(ByteBuffer[] buffers, long capacity,
        boolean direct) {
      super(buffers, capacity, direct);
    }

    @Override
    public boolean isReadOnly() {
      return true;
    }

    @Override
    public XLByteBuffer put(byte b) {
      throw new UnsupportedOperationException();
    }

    @Override
    public XLByteBuffer put(byte[] src, long offset, int length) {
      throw new UnsupportedOperationException();
    }

    @Override
    public XLByteBuffer put(ByteBuffer src) {
      throw new UnsupportedOperationException();
    }

    @Override
    public XLByteBuffer put(long index, byte b) {
      throw new UnsupportedOperationException();
    }

    @Override
    public XLByteBuffer put(XLByteBuffer src) {
      throw new UnsupportedOperationException();
    }

  }

}
